// metricgen - Universal metric context generator for ibm.d modules
package main

import (
	"bytes"
	"flag"
	"fmt"
	"go/format"
	"log"
	"os"
	"path/filepath"
	"strings"
	"text/template"

	"gopkg.in/yaml.v3"
)

// Config represents the YAML structure
type Config struct {
	Classes map[string]Class `yaml:",inline"`
}

type Class struct {
	Labels   []string  `yaml:"labels"`
	Contexts []Context `yaml:"contexts"`
}

type Context struct {
	Name           string      `yaml:"name"`
	Context        string      `yaml:"context"` // Full context name
	Family         string      `yaml:"family"`
	Title          string      `yaml:"title"`
	Units          string      `yaml:"units"`
	Type           string      `yaml:"type"`
	Priority       int         `yaml:"priority"`
	MinUpdateEvery int         `yaml:"min_update_every"` // Minimum update interval
	Dimensions     []Dimension `yaml:"dimensions"`
}

type Dimension struct {
	Name      string `yaml:"name"`
	Algorithm string `yaml:"algo"`
	Mul       int    `yaml:"mul"`
	Div       int    `yaml:"div"`
	Precision int    `yaml:"precision"`
}

const outputTemplate = `// Code generated by metricgen; DO NOT EDIT.
// source: {{.Source}}

package {{.Package}}

import (
	"strings"
	"github.com/netdata/netdata/go/plugins/plugin/ibm.d/framework"
	"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
)

// cleanLabelValue cleans a label value for use in instance/dimension IDs
func cleanLabelValue(value string) string {
	// Replace problematic characters
	r := strings.NewReplacer(
		" ", "_",
		".", "_",
		"-", "_",
		"/", "_",
		":", "_",
		"=", "_",
		",", "_",
		"(", "_",
		")", "_",
	)
	return strings.ToLower(r.Replace(value))
}

// EmptyLabels is used for contexts without labels
type EmptyLabels struct{}

// InstanceID for empty labels just returns the context name
func (EmptyLabels) InstanceID(contextName string) string {
	return contextName
}

{{range $className, $class := .Classes}}
// --- {{$className}} ---

{{range $class.Contexts}}
// {{$className}}{{.Name}}Values defines the type-safe values for {{$className}}.{{.Name}} context
type {{$className}}{{.Name}}Values struct {
{{range .Dimensions}}	{{title .Name}} int64
{{end}}}

// {{$className}}{{.Name}}Context provides type-safe operations for {{$className}}.{{.Name}} context
type {{$className}}{{.Name}}Context struct {
	framework.Context[{{if $class.Labels}}{{$className}}Labels{{else}}EmptyLabels{{end}}]
}

// Set provides type-safe dimension setting for {{$className}}.{{.Name}} context
func (c {{$className}}{{.Name}}Context) Set(state *framework.CollectorState, labels {{if $class.Labels}}{{$className}}Labels{{else}}EmptyLabels{{end}}, values {{$className}}{{.Name}}Values) {
	state.SetMetricsForGeneratedCode(&c.Context, {{if $class.Labels}}labels{{else}}nil{{end}}, map[string]int64{
{{range .Dimensions}}		"{{.Name}}": values.{{title .Name}},
{{end}}	})
}

// SetUpdateEvery sets the update interval for this instance
func (c {{$className}}{{.Name}}Context) SetUpdateEvery(state *framework.CollectorState, labels {{if $class.Labels}}{{$className}}Labels{{else}}EmptyLabels{{end}}, updateEvery int) {
	state.SetUpdateEveryOverrideForGeneratedCode(&c.Context, {{if $class.Labels}}labels{{else}}nil{{end}}, updateEvery)
}
{{end}}

{{if $class.Labels}}
// {{$className}}Labels defines the required labels for {{$className}} contexts
type {{$className}}Labels struct {
{{range $class.Labels}}	{{title .}} string
{{end}}}

// InstanceID generates a unique instance ID using the hardcoded label order from YAML
func (l {{$className}}Labels) InstanceID(contextName string) string {
	// Label order from YAML: {{range $i, $label := $class.Labels}}{{if $i}}, {{end}}{{$label}}{{end}}
	return contextName + "." + {{range $i, $label := $class.Labels}}{{if $i}} + "_" + {{end}}cleanLabelValue(l.{{title $label}}){{end}}
}
{{end}}

// {{$className}} contains all metric contexts for {{$className}}
var {{$className}} = struct {
{{range $class.Contexts}}	{{.Name}} {{$className}}{{.Name}}Context
{{end}}}{
{{range $class.Contexts}}	{{.Name}}: {{$className}}{{.Name}}Context{
		Context: framework.Context[{{if $class.Labels}}{{$className}}Labels{{else}}EmptyLabels{{end}}]{
		Name:       "{{.Context}}",
		Family:     "{{.Family}}",
		Title:      "{{.Title}}",
		Units:      "{{.Units}}",
		Type:       module.{{title .Type}},
		Priority:   {{.Priority}},
		UpdateEvery: {{if .MinUpdateEvery}}{{.MinUpdateEvery}}{{else}}1{{end}},
		Dimensions: []framework.Dimension{
{{range .Dimensions}}			{
				Name:      "{{.Name}}",
				Algorithm: module.{{title .Algorithm}},
				Mul:       {{.Mul}},
				Div:       {{.Div}},
				Precision: {{.Precision}},
			},
{{end}}		},
		LabelKeys: []string{
{{range $class.Labels}}			"{{.}}",
{{end}}		},
		},
	},
{{end}}}

{{end}}

// GetAllContexts returns all contexts for framework registration
func GetAllContexts() []interface{} {
	return []interface{}{
{{range $className, $class := .Classes}}{{range $class.Contexts}}		&{{$className}}.{{.Name}}.Context,
{{end}}{{end}}	}
}
`

func main() {
	var (
		input  = flag.String("input", "contexts.yaml", "Input YAML file")
		output = flag.String("output", "zz_generated_contexts.go", "Output Go file")
		pkg    = flag.String("package", "contexts", "Package name")
		module = flag.String("module", "", "Module prefix (e.g., as400, db2, mq)")
	)
	flag.Parse()

	// Read input file
	data, err := os.ReadFile(*input)
	if err != nil {
		log.Fatalf("failed to read input file: %v", err)
	}

	// Parse YAML
	var config Config
	if err := yaml.Unmarshal(data, &config.Classes); err != nil {
		log.Fatalf("failed to parse YAML: %v", err)
	}

	// Process the config
	processConfig(&config, *module)

	// Generate output
	if err := generateOutput(config, *input, *output, *pkg); err != nil {
		log.Fatalf("failed to generate output: %v", err)
	}

	log.Printf("Generated %s from %s", *output, *input)
}

func processConfig(config *Config, modulePrefix string) {
	// Set defaults and add module prefix if specified
	for className, class := range config.Classes {
		for i := range class.Contexts {
			ctx := &class.Contexts[i]

			// Context names should be fully qualified in the YAML file
			// This allows flexibility to move/inject contexts anywhere

			// Set default algorithm
			for j := range ctx.Dimensions {
				dim := &ctx.Dimensions[j]
				if dim.Algorithm == "" {
					dim.Algorithm = "absolute"
				}
				if dim.Mul == 0 {
					dim.Mul = 1
				}
				if dim.Div == 0 {
					dim.Div = 1
				}
				if dim.Precision == 0 {
					dim.Precision = 1
				}
			}

			// Set default min_update_every
			if ctx.MinUpdateEvery == 0 {
				ctx.MinUpdateEvery = 1
			}

			// Set default priority
			if ctx.Priority == 0 {
				ctx.Priority = 70000 // Default priority
			}

			// Set default chart type
			if ctx.Type == "" {
				ctx.Type = "line"
			}
		}
		config.Classes[className] = class
	}
}

func generateOutput(config Config, source, output, pkg string) error {
	// Parse template with custom functions
	tmpl, err := template.New("output").Funcs(template.FuncMap{
		"title": strings.Title,
	}).Parse(outputTemplate)
	if err != nil {
		return fmt.Errorf("failed to parse template: %v", err)
	}

	// Execute template into a buffer so we can run gofmt before writing
	data := struct {
		Source  string
		Package string
		Classes map[string]Class
	}{
		Source:  filepath.Base(source),
		Package: pkg,
		Classes: config.Classes,
	}

	var buf bytes.Buffer
	if err := tmpl.Execute(&buf, data); err != nil {
		return fmt.Errorf("failed to execute template: %v", err)
	}

	formatted, err := format.Source(buf.Bytes())
	if err != nil {
		return fmt.Errorf("failed to format generated code: %v", err)
	}

	if err := os.WriteFile(output, formatted, 0o644); err != nil {
		return fmt.Errorf("failed to write output file: %v", err)
	}

	return nil
}
